干货 | 计算密集型服务的负载均衡策略
作者简介
罗茂林,携程国际机票后台研发总监,主要负责国际机票引擎的研发工作。致力于系统性能优化和研发效率提升。
一般情况下,在计算密集型服务中,即使处理单个请求也需要使用到服务器的所有CPU。如果单台服务器连续接收到两个请求,要么两个请求互相争抢CPU,要么后来的请求排在前面的后面等待处理。最终,会导致平均处理时间变长。常规的负载均衡策略(如轮询、随机等)下,负载均衡器不关心服务器的负载情况,这就很容易造成服务器同时收到多个请求,从而使服务器的服务质量下降。
一、背景
有一天,携程国际机票查询引擎经过一次改造后,虽然平均响应时间得到了提升,但是响应时间也有非常大的波动。从监控图上看,非常明显的尖刺持续存在。如下图:
经过分析,我们发现这次改造深度优化了服务的并行计算能力,使得引擎成为了一个完全的计算密集型服务,它的最大并发处理能力为1。然而,我们却没有相应的修改负载均衡策略,而是继续使用的轮询策略。
对于计算密集型服务,如果使用轮询策略,有如下三种情况:
理想情况是连续两个请求之间无间隔、无重叠,既下一个请求刚好在上一个请求处理完成的时刻到达。这种情况下,后来的请求没有等待时间,服务器也没有空闲时间,得到了充分的利用。
通常情况下,由于请求的到达普遍服从泊松分布,如果使用轮询、随机等负载均衡策略,单机的请求也服从泊松分布,即连续两个请求间总会存在间隔或者重叠,导致服务器资源空闲或者请求响应时间上升。
在极端情况下,如果某个请求的处理时间特别长,后续的一大串请求将产生积压,最终导致这些请求的响应时间也变得特别长,甚至超时。
我们发现,引擎的响应时间尖刺是由极端情况的case造成的。引擎有一类请求A,它qps不高,但是却需要CPU满负荷运转长达几秒甚至10秒才能算出结果。另有一类请求B,它qps非常高,只需要CPU满负荷运转几十毫秒就能算出结果。
当一台服务器正在处理一个A类请求时,在接下来的几秒内,它将继续收到几十个B类请求,而且所有的B类请求都要排队,直到A类请求完成。这就导致大批B类请求的响应时间由应该的几十毫秒升高到几秒,从而造成了严重的尖刺。
二、pooling
为了解决这个问题,我们使用了一种新的负载均衡策略,在这种策略下,服务器不再被动的接收请求,而是主动的去获取请求,这种方式非常容易做到服务器同一时刻只处理一个请求。在我们内部,这种方式被称为pooling(它和线程池类似,可以叫做服务器池)。
在pooling模式中,有三个主要角色:submitor、queue、worker。
submitor
submitor一方面用于接收请求方的调用,它收到请求后,不直接处理请求,而是把这个请求提交给queue。
另一方面,submitor接收worker的回调,submitor收到worker的结果后,直接把它转发给请求方。
queue
pooling的关键是引入了一个queue,queue是一个全局唯一队列,用于暂时缓冲请求。
我们使用了redis的list结构来实现queue。入队操作为lpush,出队操作为brpop。brpop是阻塞式的操作,当队列为空时,brpop会阻塞直到队列非空。队列非空时,如果有该队列有多个brpop操作阻塞,只有其中一个会被唤醒并且返回数据。
worker
worker是实际的请求处理者。在旧的模式下,worker是被动接收请求。在pooling模式下,worker要主动去queue获取请求。worker启动时,要创建一个线程,这个线程启动后,便进入一个无限循环,循环的主要内容为:
1)从queue获取一个请求,当queue没有请求时,worker被阻塞。
2)worker处理这个请求。
3)把结果返回给submitor。
如此往复。可以看到,worker要么正在处理一个请求,要么正在等待一个请求。
三、效果
国际机票查询引擎的负载均衡策略由轮询改为pooling后,效果非常好。系统的平均响应时间降低了大约20%,并且完全消除了响应时间尖刺。
轮询方式:
pooling方式:
【推荐阅读】
“携程技术中心”公众号
分享,交流,成长